/* ========================================================================
 * Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
 *
 * OPC Foundation MIT License 1.00
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 *
 * The complete license agreement can be found here:
 * http://opcfoundation.org/License/MIT/1.00/
 * ======================================================================*/

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using Opc.Ua.PubSub.Encoding;
using Opc.Ua.PubSub.Transport;
using Opc.Ua.Tests;
using Assert = NUnit.Framework.Legacy.ClassicAssert;

namespace Opc.Ua.PubSub.Tests.Transport
{
    [TestFixture(Description = "Tests for UdpPubSubConnection class")]
    public partial class UdpPubSubConnectionTests
    {
        private const int kEstimatedPublishingTime = 10000;

        private const string kUdpUrlFormat = "{0}://{1}:4840";
        private const string kUdpDiscoveryIp = "224.0.2.14";
        private const string kUdpMulticastIp = "239.0.0.1";
        private const int kDiscoveryPortNo = 4840;

        protected enum UdpConnectionType
        {
            Networking,
            Discovery
        }

        protected enum UdpAddressesType
        {
            Unicast,
            Broadcast,
            Multicast
        }

        protected enum UadpDiscoveryType
        {
            Request,
            Response
        }

        private readonly string m_publisherConfigurationFileName = Path.Combine(
            "Configuration",
            "PublisherConfiguration.xml");

        private readonly string m_subscriberConfigurationFileName = Path.Combine(
            "Configuration",
            "SubscriberConfiguration.xml");

        private PubSubConfigurationDataType m_publisherConfiguration;
        private UaPubSubApplication m_uaPublisherApplication;
        private UdpPubSubConnection m_udpPublisherConnection;
        private ServiceMessageContext m_messageContext;
        private ILogger m_logger;
        private ManualResetEvent m_shutdownEvent;

        /// <summary>
        /// private UdpAddressesType m_udpAddressesType = UdpAddressesType.Unicast;
        /// </summary>
        [OneTimeSetUp]
        public void MyTestInitialize()
        {
            ITelemetryContext telemetry = NUnitTelemetryContext.Create();
            m_messageContext = new ServiceMessageContext(telemetry);
            m_logger = telemetry.CreateLogger<UdpPubSubConnectionTests>();

            // Create a publisher application
            string configurationFile = Utils.GetAbsoluteFilePath(
                m_publisherConfigurationFileName,
                checkCurrentDirectory: true,
                createAlways: false);
            m_uaPublisherApplication = UaPubSubApplication.Create(configurationFile, null);
            Assert.IsNotNull(m_uaPublisherApplication, "m_publisherApplication should not be null");

            // Get the publisher configuration
            m_publisherConfiguration = m_uaPublisherApplication.UaPubSubConfigurator
                .PubSubConfiguration;
            Assert.IsNotNull(
                m_publisherConfiguration,
                "m_publisherConfiguration should not be null");

            // Get publisher connection
            Assert.IsNotNull(
                m_publisherConfiguration.Connections,
                "m_publisherConfiguration.Connections should not be null");
            Assert.IsNotEmpty(
                m_publisherConfiguration.Connections,
                "m_publisherConfiguration.Connections should not be empty");
            m_udpPublisherConnection = m_uaPublisherApplication.PubSubConnections[
                0] as UdpPubSubConnection;
            Assert.IsNotNull(
                m_udpPublisherConnection,
                "m_uadpPublisherConnection should not be null");
        }

        [Test(Description = "Validate TransportProtocol value")]
        public void ValidateUdpPubSubConnectionTransportProtocol()
        {
            //Assert
            Assert.IsNotNull(
                m_udpPublisherConnection,
                "The UDP connection from standard configuration is invalid.");
            Assert.IsTrue(
                m_udpPublisherConnection.TransportProtocol == TransportProtocol.UDP,
                "The UADP connection has wrong TransportProtocol {0}",
                m_udpPublisherConnection.TransportProtocol);
        }

        [Test(Description = "Validate PubSubConnectionConfiguration value")]
        public void ValidateUdpPubSubConnectionPubSubConnectionConfiguration()
        {
            //Assert
            Assert.IsNotNull(
                m_udpPublisherConnection,
                "The UADP connection from standard configuration is invalid.");
            PubSubConnectionDataType connectionConfiguration = m_udpPublisherConnection
                .PubSubConnectionConfiguration;
            PubSubConnectionDataType originalConnectionConfiguration = m_publisherConfiguration
                .Connections[0];
            Assert.IsNotNull(
                connectionConfiguration,
                "The UADP connection configuration from UADP connection object is invalid.");
            Assert.AreEqual(
                originalConnectionConfiguration.Name,
                connectionConfiguration.Name,
                "The connection configuration Name is invalid.");
            Assert.AreEqual(
                originalConnectionConfiguration.PublisherId,
                connectionConfiguration.PublisherId,
                "The connection configuration PublisherId is invalid.");
            Assert.AreEqual(
                originalConnectionConfiguration.Address,
                connectionConfiguration.Address,
                "The connection configuration Address is invalid.");
            Assert.AreEqual(
                originalConnectionConfiguration.Enabled,
                connectionConfiguration.Enabled,
                "The connection configuration Enabled is invalid.");
            Assert.AreEqual(
                originalConnectionConfiguration.TransportProfileUri,
                connectionConfiguration.TransportProfileUri,
                "The connection configuration TransportProfileUri is invalid.");
        }

        [Test(Description = "Validate Application value")]
        public void ValidateUdpPubSubConnectionApplication()
        {
            //Assert
            Assert.IsNotNull(
                m_udpPublisherConnection,
                "The UADP connection from standard configuration is invalid.");
            Assert.AreEqual(
                m_udpPublisherConnection.Application,
                m_uaPublisherApplication,
                "The UADP connection Application reference is invalid.");
        }

        [Test(Description = "Validate Publishers value")]
        public void ValidateUdpPubSubConnectionPublishers()
        {
            //Assert
            Assert.IsNotNull(
                m_udpPublisherConnection,
                "The UADP connection from standard configuration is invalid.");
            Assert.IsNotNull(
                m_udpPublisherConnection.Publishers,
                "The UADP connection Publishers is invalid.");
            Assert.AreEqual(
                1,
                m_udpPublisherConnection.Publishers.Count,
                "The UADP connection Publishers.Count is invalid.");
            int index = 0;
            foreach (IUaPublisher publisher in m_udpPublisherConnection.Publishers)
            {
                Assert.IsTrue(publisher != null, "connection.Publishers[{0}] is null", index);
                Assert.IsTrue(
                    publisher.PubSubConnection == m_udpPublisherConnection,
                    "connection.Publishers[{0}].PubSubConnection is not set correctly",
                    index);
                Assert.IsTrue(
                    publisher.WriterGroupConfiguration.WriterGroupId ==
                        m_publisherConfiguration.Connections[0].WriterGroups[index].WriterGroupId,
                    "connection.Publishers[{0}].WriterGroupConfiguration is not set correctly",
                    index);
                index++;
            }
        }

        [Test(Description = "Validate CreateNetworkMessage")]
        public void ValidateUdpPubSubConnectionCreateNetworkMessage()
        {
            Assert.IsNotNull(
                m_udpPublisherConnection,
                "The UADP connection from standard configuration is invalid.");

            //Arrange
            WriterGroupDataType writerGroup0 = m_udpPublisherConnection
                .PubSubConnectionConfiguration
                .WriterGroups[0];
            var messageSettings =
                ExtensionObject.ToEncodeable(
                    writerGroup0.MessageSettings) as UadpWriterGroupMessageDataType;

            //Act
            UdpPubSubConnection.ResetSequenceNumber();

            IList<UaNetworkMessage> networkMessages = m_udpPublisherConnection
                .CreateNetworkMessages(
                    writerGroup0,
                    new WriterGroupPublishState());
            Assert.IsNotNull(
                networkMessages,
                "connection.CreateNetworkMessages shall not return null");
            UaNetworkMessage networkMessagesNetworkType = networkMessages.FirstOrDefault(
                net => !net.IsMetaDataMessage);
            Assert.IsNotNull(
                networkMessagesNetworkType,
                "connection.CreateNetworkMessages shall return only one network message");

            var networkMessage0 = networkMessagesNetworkType as UadpNetworkMessage;
            Assert.IsNotNull(networkMessage0, "networkMessageEncode should not be null");

            //Assert
            Assert.IsNotNull(
                networkMessage0,
                "CreateNetworkMessage did not return an UadpNetworkMessage.");

            Assert.AreEqual(
                networkMessage0.DataSetClassId,
                Guid.Empty,
                "UadpNetworkMessage.DataSetClassId is invalid.");
            Assert.AreEqual(
                networkMessage0.WriterGroupId,
                writerGroup0.WriterGroupId,
                "UadpNetworkMessage.WriterGroupId is invalid.");
            Assert.AreEqual(
                networkMessage0.UADPVersion,
                1,
                "UadpNetworkMessage.UADPVersion is invalid.");
            Assert.AreEqual(
                networkMessage0.SequenceNumber,
                1,
                "UadpNetworkMessage.SequenceNumber is not 1.");
            Assert.AreEqual(
                networkMessage0.GroupVersion,
                messageSettings.GroupVersion,
                "UadpNetworkMessage.GroupVersion is not valid.");
            Assert.AreEqual(
                networkMessage0.PublisherId,
                m_udpPublisherConnection.PubSubConnectionConfiguration.PublisherId.Value,
                "UadpNetworkMessage.PublisherId is not valid.");
            Assert.IsNotNull(
                networkMessage0.DataSetMessages,
                "UadpNetworkMessage.UadpDataSetMessages is null.");
            Assert.AreEqual(
                networkMessage0.DataSetMessages.Count,
                3,
                "UadpNetworkMessage.UadpDataSetMessages.Count is not 3.");
            //validate flags
            Assert.AreEqual(
                (uint)networkMessage0.NetworkMessageContentMask,
                messageSettings.NetworkMessageContentMask,
                "UadpNetworkMessage.messageSettings.NetworkMessageContentMask is not valid.");
        }

        [Test(Description = "Validate CreateNetworkMessage SequenceNumber increment")]
        public void ValidateUdpPubSubConnectionCreateNetworkMessageSequenceNumber()
        {
            Assert.IsNotNull(
                m_udpPublisherConnection,
                "The UADP connection from standard configuration is invalid.");
            //Arrange
            WriterGroupDataType writerGroup0 = m_udpPublisherConnection
                .PubSubConnectionConfiguration
                .WriterGroups[0];

            //Act
            UdpPubSubConnection.ResetSequenceNumber();
            for (int i = 0; i < 10; i++)
            {
                // Create network message
                IList<UaNetworkMessage> networkMessages = m_udpPublisherConnection
                    .CreateNetworkMessages(
                        writerGroup0,
                        new WriterGroupPublishState());
                Assert.IsNotNull(
                    networkMessages,
                    "connection.CreateNetworkMessages shall not return null");
                UaNetworkMessage networkMessagesNetworkType = networkMessages.FirstOrDefault(net =>
                    !net.IsMetaDataMessage);
                Assert.IsNotNull(
                    networkMessagesNetworkType,
                    "connection.CreateNetworkMessages shall return only one network message");

                var networkMessage = networkMessagesNetworkType as UadpNetworkMessage;
                Assert.IsNotNull(networkMessage, "networkMessageEncode should not be null");

                //Assert
                Assert.IsNotNull(
                    networkMessage,
                    "CreateNetworkMessage did not return an UadpNetworkMessage.");
                Assert.AreEqual(
                    networkMessage.SequenceNumber,
                    i + 1,
                    "UadpNetworkMessage.SequenceNumber for message {0} is not {0}.",
                    i + 1);

                //validate dataset message sequence number
                Assert.IsNotNull(
                    networkMessage.DataSetMessages,
                    "CreateNetworkMessage did not return an UadpNetworkMessage.UadpDataSetMessages.");
                Assert.IsTrue(
                    networkMessage.DataSetMessages.Count == 3,
                    "CreateNetworkMessage did not return 3 UadpNetworkMessage.UadpDataSetMessages.");
                Assert.AreEqual(
                    networkMessage.DataSetMessages[0].SequenceNumber,
                    (i * 3) + 1,
                    "UadpNetworkMessage.UadpDataSetMessages[0].SequenceNumber for message {0} is not {1}.",
                    i + 1,
                    (i * 3) + 1);
                Assert.AreEqual(
                    networkMessage.DataSetMessages[1].SequenceNumber,
                    (i * 3) + 2,
                    "UadpNetworkMessage.UadpDataSetMessages[1].SequenceNumber for message {0} is not {1}.",
                    i + 1,
                    (i * 3) + 2);
                Assert.AreEqual(
                    networkMessage.DataSetMessages[2].SequenceNumber,
                    (i * 3) + 3,
                    "UadpNetworkMessage.UadpDataSetMessages[2].SequenceNumber for message {0} is not {1}.",
                    i + 1,
                    (i * 3) + 3);
            }
        }

        /// <summary>
        /// Get localhost address reference
        /// </summary>
        public static UnicastIPAddressInformation GetFirstNic()
        {
            string activeIp = "127.0.0.1";

            IPAddress firstActiveIPAddr = GetFirstActiveNic();
            if (firstActiveIPAddr != null)
            {
                activeIp = firstActiveIPAddr.ToString();
            }

            foreach (NetworkInterface nic in NetworkInterface.GetAllNetworkInterfaces())
            {
                if (nic.NetworkInterfaceType != NetworkInterfaceType.Loopback &&
                    nic.OperationalStatus == OperationalStatus.Up)
                {
                    foreach (UnicastIPAddressInformation addr in nic.GetIPProperties()
                        .UnicastAddresses)
                    {
                        if (addr.Address.ToString().Contains(activeIp, StringComparison.Ordinal))
                        {
                            // return specified address
                            return addr;
                        }
                    }
                }
            }

            return null;
        }

        /// <summary>
        /// Data received handler
        /// </summary>
        private void UaPubSubApplication_DataReceived(object sender, SubscribedDataEventArgs e)
        {
            m_shutdownEvent.Set();
        }

        /// <summary>
        /// Get first active broadcast ip
        /// </summary>
        private static IPAddress GetFirstNicLastIPByteChanged(byte lastIpByte)
        {
            IPAddress firstActiveIPAddr = GetFirstActiveNic();
            if (firstActiveIPAddr != null)
            {
                // replace last IP byte from address with 255 (broadcast)
                bool isValidIP = IPAddress.TryParse(
                    firstActiveIPAddr.ToString(),
                    out IPAddress validIp);
                if (isValidIP)
                {
                    byte[] ipAddressBytes = validIp.GetAddressBytes();
                    ipAddressBytes[^1] = lastIpByte;
                    return new IPAddress(ipAddressBytes);
                }
            }

            return null;
        }

        /// <summary>
        /// Check if the specified ip is a local host ip
        /// </summary>
        private static bool IsHostAddress(string ipAddress)
        {
            string hostName = Dns.GetHostName();
            foreach (IPAddress address in Dns.GetHostEntry(hostName).AddressList)
            {
                if (address.MapToIPv4().ToString().Equals(ipAddress, StringComparison.Ordinal))
                {
                    return true;
                }
            }
            return false;
        }

        /// <summary>
        /// Get list of active IPv4 addresses.
        /// </summary>
        public static IPAddress[] GetLocalIpAddresses()
        {
            var addresses = new List<IPAddress>();
            foreach (NetworkInterface netI in NetworkInterface.GetAllNetworkInterfaces())
            {
                if (netI.NetworkInterfaceType != NetworkInterfaceType.Wireless80211 &&
                    (
                        netI.NetworkInterfaceType != NetworkInterfaceType.Ethernet ||
                        netI.OperationalStatus != OperationalStatus.Up))
                {
                    continue;
                }
                if (netI.GetIPProperties().GatewayAddresses.Count == 0)
                {
                    continue;
                }
                foreach (UnicastIPAddressInformation uniIpAddrInfo in netI.GetIPProperties()
                    .UnicastAddresses)
                {
                    if (uniIpAddrInfo.Address.AddressFamily
                        is AddressFamily.InterNetwork
                            or AddressFamily.InterNetworkV6)
                    {
                        if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) &&
                            (uniIpAddrInfo.AddressPreferredLifetime == uint.MaxValue))
                        {
                            continue;
                        }
                        addresses.Add(uniIpAddrInfo.Address);
                    }
                }
            }
            return [.. addresses];
        }

        /// <summary>
        /// Get first active nic on local computer
        /// </summary>
        private static IPAddress GetFirstActiveNic()
        {
            try
            { // get host IP addresses
                IPAddress[] hostIPs = Dns.GetHostAddresses(Dns.GetHostName());
                // get local IP addresses
                IPAddress[] localIPs = GetLocalIpAddresses();

                // test if any host IP equals to any local IP or to localhost
                foreach (IPAddress hostIP in hostIPs)
                {
                    // is loopback type?
                    if (IPAddress.IsLoopback(hostIP))
                    {
                        continue;
                    }
                    // ip address available
                    foreach (IPAddress localIP in localIPs)
                    {
                        if (localIP.AddressFamily == AddressFamily.InterNetwork &&
                            hostIP.Equals(localIP))
                        {
                            return localIP;
                        }
                    }
                }
            }
            catch
            {
            }
            NUnit.Framework.Assert.Inconclusive("First active NIC was not found.");

            return null;
        }

        [Test(Description = "Validate UDP client socket access before connection is started")]
        public void ValidateUdpPubSubConnectionSocketAccessBeforeStart()
        {
            // Arrange
            Assert.IsNotNull(
                m_udpPublisherConnection,
                "The UADP connection from standard configuration is invalid.");

            // Act - Access clients before connection is started
            IReadOnlyList<UdpClient> publisherClients = m_udpPublisherConnection.PublisherUdpClients;
            IReadOnlyList<UdpClient> subscriberClients = m_udpPublisherConnection.SubscriberUdpClients;

            // Assert - Should return empty lists before connection is started
            Assert.IsNotNull(publisherClients, "PublisherUdpClients should not be null");
            Assert.IsNotNull(subscriberClients, "SubscriberUdpClients should not be null");
            Assert.AreEqual(0, publisherClients.Count, "PublisherUdpClients should be empty before start");
            Assert.AreEqual(0, subscriberClients.Count, "SubscriberUdpClients should be empty before start");
        }

        [Test(Description = "Validate UDP client socket access after connection is started")]
        public void ValidateUdpPubSubConnectionSocketAccessAfterStart()
        {
            // Arrange
            Assert.IsNotNull(
                m_udpPublisherConnection,
                "The UADP connection from standard configuration is invalid.");

            // Act - Start the connection
            m_udpPublisherConnection.Start();

            try
            {
                // Access clients after connection is started
                IReadOnlyList<UdpClient> publisherClients = m_udpPublisherConnection.PublisherUdpClients;
                IReadOnlyList<UdpClient> subscriberClients = m_udpPublisherConnection.SubscriberUdpClients;

                // Assert - Should have clients after connection is started
                Assert.IsNotNull(publisherClients, "PublisherUdpClients should not be null");
                Assert.IsNotNull(subscriberClients, "SubscriberUdpClients should not be null");

                // Publisher should have clients since there are publishers configured
                if (m_udpPublisherConnection.Publishers.Count > 0)
                {
                    Assert.Greater(publisherClients.Count, 0, "PublisherUdpClients should not be empty when publishers exist");

                    // Verify we can access the underlying socket
                    foreach (UdpClient client in publisherClients)
                    {
                        Assert.IsNotNull(client, "UDP client should not be null");
                        Assert.IsNotNull(client.Client, "UDP client Socket should not be null");

                        // Verify we can read socket properties (e.g., ReceiveBufferSize)
                        int receiveBufferSize = client.Client.ReceiveBufferSize;
                        Assert.Greater(receiveBufferSize, 0, "ReceiveBufferSize should be greater than 0");

                        m_logger.LogInformation(
                            "Publisher UDP Socket - ReceiveBufferSize: {Size}, LocalEndPoint: {Endpoint}",
                            receiveBufferSize,
                            client.Client.LocalEndPoint);
                    }
                }
            }
            finally
            {
                // Cleanup - Stop the connection
                m_udpPublisherConnection.Stop();
            }
        }

        [Test(Description = "Validate that UDP client list is read-only")]
        public void ValidateUdpPubSubConnectionSocketListIsReadOnly()
        {
            // Arrange
            Assert.IsNotNull(
                m_udpPublisherConnection,
                "The UADP connection from standard configuration is invalid.");

            // Act
            IReadOnlyList<UdpClient> publisherClients = m_udpPublisherConnection.PublisherUdpClients;
            IReadOnlyList<UdpClient> subscriberClients = m_udpPublisherConnection.SubscriberUdpClients;

            // Assert - The returned collections should be read-only
            Assert.IsNotNull(publisherClients, "PublisherUdpClients should not be null");
            Assert.IsNotNull(subscriberClients, "SubscriberUdpClients should not be null");

            // Verify that the collections are truly read-only (no Add/Remove methods exposed)
            Assert.IsInstanceOf<IReadOnlyList<UdpClient>>(publisherClients, "PublisherUdpClients should be IReadOnlyList");
            Assert.IsInstanceOf<IReadOnlyList<UdpClient>>(subscriberClients, "SubscriberUdpClients should be IReadOnlyList");
        }
    }
}
